import type { FeatureCollection, Feature } from 'geojson'
import mapboxgl, {
  GeoJSONSource,
  type SymbolLayer,
  type LineLayer,
  type FillLayer
} from 'mapbox-gl'
import type { Layer } from '../types'
import type { FillFeature, MapFeatures, LineFeature, SymbolFeature } from './feature'

abstract class BaseGeojsonLayer<F extends Feature> implements Layer {
  readonly id: string
  readonly name: string
  protected map: mapboxgl.Map | undefined
  private readonly data: FeatureCollection = {
    type: 'FeatureCollection',
    features: []
  }
  private isVisible: boolean
  private before: string | null
  constructor(id: string, name: string, visible?: boolean, before?: string | null | undefined) {
    this.id = id
    this.name = name
    this.isVisible = visible == null ? true : visible
    this.before = before === undefined ? 'overlay#fill' : before
  }

  public get visible(): boolean {
    return this.isVisible
  }

  // 设置图层的可见性
  public set visible(ok: boolean) {
    if (this.isVisible === ok) {
      return
    }
    this.isVisible = ok
    if (this.map) {
      this.map.setLayoutProperty(this.id, 'visibility', this.isVisible ? 'visible' : 'none')
    }
  }

  public set(f: F | undefined) {
    if (f === undefined) {
      return
    }
    this.data.features = [f]
    this.repaint()
  }

  public updateAll(lst: Array<F>) {
    if (this.data.features === lst) {
      return
    }
    if (lst.length === 0 && this.data.features.length === 0) {
      return
    }
    this.data.features = lst
    this.repaint()
  }

  public updateLast(f: F) {
    const lst = this.data.features
    if (lst.length === 0) {
      return
    }
    lst[lst.length - 1] = f
    this.repaint()
  }

  public updateAt(idx: number, f: F) {
    if (idx < 0 || idx >= this.data.features.length) {
      return
    }
    this.data.features[idx] = f
    this.repaint()
  }

  public addAll(lst: F[]) {
    if (!lst || lst.length === 0) {
      return
    }
    this.data.features.push(...lst)
    this.repaint()
  }

  public add(f: F) {
    this.data.features.push(f)
    this.repaint()
  }

  public attach(m: mapboxgl.Map) {
    if (this.map) {
      return
    }
    this.map = m
    const layer = this.makeLayer() as mapboxgl.Layer
    layer.source = { type: 'geojson', data: this.data }
    layer.id = this.id
    this.map.addLayer(layer as mapboxgl.AnyLayer, this.before || undefined)
  }

  public detach() {
    if (!this.map) {
      return
    }
    this.map.removeLayer(this.id)
    this.map.removeSource(this.id)
    this.map = undefined
  }

  public clear() {
    if (this.data.features.length === 0) {
      return
    }
    this.data.features = []
    this.repaint()
  }

  private repaint() {
    if (this.map) {
      ;(this.map.getSource(this.id) as GeoJSONSource).setData(this.data)
    }
  }

  protected abstract makeLayer(): Omit<
    mapboxgl.SymbolLayer | mapboxgl.LineLayer | mapboxgl.FillLayer,
    'source'
  >
}

export class GeojsonSymbolLayer extends BaseGeojsonLayer<SymbolFeature> {
  private fontScale = 1.0

  public setFontScale(fs: number) {
    this.fontScale = fs
    if (this.map) {
      this.map.setLayoutProperty(this.id, 'text-field', [
        'format',
        ['get', 'text'],
        { 'font-scale': this.fontScale }
      ])
    }
  }

  protected makeLayer(): Omit<SymbolLayer | LineLayer | FillLayer, 'source'> {
    return {
      id: this.id,
      type: 'symbol',
      layout: {
        'icon-image': ['get', 'iconImage'],
        'text-field': ['format', ['get', 'text'], { 'font-scale': this.fontScale }],
        'text-offset': ['get', 'textOffSet'],
        'text-anchor': 'bottom',
        'icon-allow-overlap': true,
        'text-allow-overlap': true
      },
      paint: {
        'text-color': ['get', 'textColor']
      }
    }
  }
}

export class GeojsonLineLayer extends BaseGeojsonLayer<LineFeature> {
  protected makeLayer(): Omit<mapboxgl.LineLayer, 'source'> {
    return {
      id: this.id,
      type: 'line',
      paint: {
        'line-color': ['get', 'lineColor'],
        'line-width': ['get', 'lineWidth'],
        'line-opacity': ['get', 'lineOpacity']
      }
    }
  }
}

export class GeojsonFillLayer extends BaseGeojsonLayer<FillFeature> {
  protected makeLayer(): Omit<mapboxgl.FillLayer, 'source'> {
    return {
      id: this.id,
      type: 'fill',
      layout: {},
      paint: {
        'fill-color': ['get', 'color'],
        'fill-opacity': ['get', 'opacity'],
        'fill-outline-color': ['get', 'outlineColor']
      }
    }
  }
}

export class GeojsonLayers implements Layer {
  public readonly id: string
  public readonly name: string

  private symbol: GeojsonSymbolLayer
  private line: GeojsonLineLayer
  private fill: GeojsonFillLayer

  constructor(id: string, name: string, visible?: boolean, before?: string | null | undefined) {
    this.id = id
    this.name = name
    this.symbol = new GeojsonSymbolLayer(`${id}#symbol`, `${name}#symbol`, visible, before)
    this.line = new GeojsonLineLayer(`${id}#line`, `${name}#line`, visible, before)
    this.fill = new GeojsonFillLayer(`${id}#fill`, `${name}#fill`, visible, before)
  }

  get visible(): boolean {
    return this.symbol.visible
  }

  set visible(ok: boolean) {
    this.symbol.visible = ok
    this.line.visible = ok
    this.fill.visible = ok
  }

  public setSymbolFontScale(fs: number) {
    this.symbol.setFontScale(fs)
  }

  public attach(m: mapboxgl.Map): void {
    this.fill.attach(m)
    this.line.attach(m)
    this.symbol.attach(m)
  }

  public clear() {
    this.symbol.clear()
    this.line.clear()
    this.fill.clear()
  }

  public updateLine(l: LineFeature) {
    this.line.set(l)
  }

  public updateLines(ls: LineFeature[]) {
    this.line.updateAll(ls)
  }

  public updateSymbol(s: SymbolFeature) {
    this.symbol.set(s)
  }

  public updateSymbols(s: SymbolFeature[]) {
    this.symbol.updateAll(s)
  }

  public addSymbol(s: SymbolFeature) {
    this.symbol.add(s)
  }

  public addSymbols(ss: SymbolFeature[]) {
    this.symbol.addAll(ss)
  }

  public add(fs: MapFeatures) {
    const { symbol, line, fill } = fs
    if (symbol) {
      this.symbol.addAll(symbol)
    }
    if (line) {
      this.line.addAll(line)
    }
    if (fill) {
      this.fill.addAll(fill)
    }
  }

  public update(fs: MapFeatures) {
    const { symbol, line, fill } = fs
    if (symbol) {
      this.symbol.updateAll(symbol)
    } else {
      this.symbol.clear()
    }
    if (line) {
      this.line.updateAll(line)
    } else {
      this.line.clear()
    }
    if (fill) {
      this.fill.updateAll(fill)
    } else {
      this.fill.clear()
    }
  }

  public detach() {
    this.fill.detach()
    this.line.detach()
    this.fill.detach()
  }
}
